/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package io.milton.common;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;

/**
 * Immutable class which represents a path in the file system.
 */
public class Path implements Serializable {

    private static final long serialVersionUID = -8411900835514833454L;
    private final Path parent;
    private final String name;
    public static final Path root = new Path();
    private int hash;
    private final int length;
    public static final LengthComparator LENGTH_COMPARATOR = new LengthComparator();


    /**
     * Method which creates and instance of Path.
     * @param parent Path to parent.
     * @param path relative path from parent.
     * @return Path instance.
     */
    public static Path path(Path parent, String path) {
        if (path == null)
            throw new NullPointerException("The path parameter may not be null");
        return split(parent, path);
    }

    /**
     * Method which creates and instance of Path.
     * @param path relative path from root.
     * @return Path instance.
     */
    public static Path path(String path) {
        if (path == null || path.isEmpty()) return root;
        return split(null, path);
    }

    private static Path split(Path startFrom, String s) {
        Path parent = startFrom;
        StringBuilder sb = null;
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            switch (c) {
                case '/':
                    if (i == 0) {
                        parent = root;
                    } else {
                        if (sb != null && sb.length() > 0) {
                            String ss = sb.toString();
                            if (parent != null) parent = parent.child(ss);
                            else parent = new Path(null, ss);
                        }
                        sb = null;
                    }
                    break;
//                case ' ':
//                    // ignore
//                    break;
                default:
                    if (sb == null) sb = new StringBuilder();
                    sb.append(c);
            }
        }
        if (sb != null && sb.length() > 0) {
            String ss = sb.toString();
            if (parent != null) parent = parent.child(ss);
            else parent = new Path(null, ss);
        }

        return parent;
    }

    private Path() {
        this.parent = null;
        this.name = null;
        length = 0;
    }

    private Path(Path parent, String name) {
        if (name == null)
            throw new IllegalArgumentException("name may not be null");
        this.parent = parent;
        this.name = name;
        if (this.parent != null) {
            this.length = this.parent.length + 1;
        } else {
            this.length = 1;
        }
    }


    public int getLength() {
        return length;
    }

    public String[] getParts() {
        String[] arr = new String[length];
        Path p = this;
        int i = length;
        while (i > 0) {
            arr[--i] = p.getName();
            p = p.getParent();
        }
        return arr;
    }

    /**
     * @return - the first part of the path. ie a/b/c returns a
     */
    public String getFirst() {
        Path p = this;
        while (p.getParent() != null) {
            Path next = p.getParent();
            if (next.getName() == null) return p.getName();
            p = next;
        }
        return p.getName();
    }

    public List<String> getAfterFirst() {
        List<String> afterFirst = new ArrayList<>();
        Path p = this;
        while (p.getParent() != null && !p.getParent().isRoot()) {
            afterFirst.add(0, p.getName());
            p = p.getParent();
            if (p == null) break;
        }
        return afterFirst;
    }

    public Path getStripFirst() {
        return stripFirst(this);
    }

    Path stripFirst(Path p) {
        Path pParent = p.getParent();
        if (pParent == null || pParent.isRoot()) return root;
        pParent = stripFirst(pParent);
        return new Path(pParent, p.getName());
    }

    public String getName() {
        return name;
    }

    public Path getParent() {
        return parent;
    }

    public boolean isRoot() {
        return ((parent == null) && (name == null));
    }

    public String toPath() {
        if (isRoot()) return "";
        if (parent == null) return name;
        return parent.toString() + '/' + name;
    }

    @Override
    public String toString() {
        return toPath();
    }

    public String toString(String delimiter) {
        if (parent == null) return "";
        return parent.toString(delimiter) + delimiter + name;
    }

    public static Path root() {
        return root;
    }

    @Override
    public int hashCode() {
        if (hash == 0) {
            if (parent == null) {
                hash = 158;
            } else {
                hash = parent.hashCode() ^ name.hashCode();
            }
        }
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) return false;
        if (obj instanceof Path) {
            Path p2 = (Path) obj;
            if (this.isRoot()) {
                return p2.isRoot();
            } else {
                if (parentEquals(this, p2)) {
                    return Objects.equals(this.name, p2.name);
                } else {
                    return false;
                }
            }
        } else {
            return false;
        }
    }

    private static boolean parentEquals(Path p1, Path p2) {
        if (p2.parent == null) {
            return p1.parent == null;
        } else {
            return p2.parent.equals(p1.parent);
        }
    }

    public Path child(String name) {
        return new Path(this, name);
    }

    public boolean isRelative() {
        if (parent == null) {
            return !isRoot();
        } else {
            return parent.isRelative();
        }
    }

    /**
     * Add the path components of the given path to this one.
     * <p>
     * Eg "a/b/c" + "/d/e/f" = "a/b/c/d/e/f"
     *
     * @param p
     * @return
     */
    public Path add(Path p) {
        Path x = this;
        for (String s : p.getParts()) {
            x = x.child(s);
        }
        return x;
    }

    public static class LengthComparator implements Comparator<Path> {

        public int compare(Path o1, Path o2) {
            Integer i1 = o1.getLength();
            Integer i2 = o2.getLength();
            return i1.compareTo(i2);
        }
    }
}
